#!/usr/bin/perl -w
####################################################################################################

use strict;
use warnings;

use File::Basename;
use Getopt::Long;
use FindBin qw($Bin);

####################################################################################################
# The only setting that most people would ever set is $do_nothing which will make the script
# print a list of files/directories/receipts it would remove, instead of doing anything.
####################################################################################################

my $do_nothing     = 0;
my $verbose        = 0;
my $warning        = 0;
my $debug          = 0;

GetOptions(
    'do-nothing' => \$do_nothing,
    'verbose' => \$verbose,
    'warning' => \$warning,
    'debug' => \$debug,
);

####################################################################################################

my $receipts_dir = '/Library/Receipts';
my $uninstaller_dir = $Bin;
my $uninstaller_dir_basename = basename($uninstaller_dir);
my $developer_dir = dirname($uninstaller_dir);
my $installedfiles_dir = "$developer_dir/Library/installedfiles";

####################################################################################################

my %devtools_package_hash = ();
my %package_hash = ();
my %devtools_file_hash = ();

####################################################################################################
# Determine if we are authorized to uninstall the devtools packages.
####################################################################################################

$| = 1;
if (($do_nothing == 0) && ($< != 0)) {
    die("ERROR: Must be run with root permissions -- prefix command with 'sudo'.\n");
}

####################################################################################################
# Determine whether we are running from a valid developer folder.
####################################################################################################

if (($uninstaller_dir_basename ne 'Library') || ($developer_dir eq '/')) {
    die("ERROR: Uninstaller script must be located in a folder named <developer-folder>/Library\n");
}

####################################################################################################

sub main {
    print("Start time: ".`date`);

    analyze_devtools_packages();
    remove_devtools_files();
    remove_devtools_package_receipts();
    remove_generated_files();
    remove_library_cache_xcode_files();
    remove_empty_devtools_directories();

    print("Finish time: ".`date`);
}

####################################################################################################

sub analyze_devtools_packages {
    get_devtools_package_ids();
    foreach my $package_id (sort keys %devtools_package_hash) {
        print("Analyzing devtools package: '$package_id'...\n");
        analyze_package('add', $package_id);
    }
}

####################################################################################################

sub get_devtools_package_ids {
    if ($installedfiles_dir && opendir(DIR,$installedfiles_dir)) {
        while (my $file_name = readdir(DIR)) {
            if (($file_name ne '.') && ($file_name ne '..') && ($file_name =~ m|^com\.apple\.pkg\.(.*)$|)) {
                my $package_id = $file_name;
                $devtools_package_hash{$package_id} = '';
            }
        }
        closedir(DIR);
    }
}

####################################################################################################

sub analyze_package {
    my $action = shift;
    my $package_id = shift;

    if ($package_id =~ m|^com\.apple\.pkg\.(.*)$|) {
        my $installed_dir = '';
        my $count = 0;
        if (open(PKGFILES, "< $installedfiles_dir/$package_id")) {
            while (<PKGFILES>) {
                chomp;
                if (++$count == 1) {
                    $installed_dir = "/$_";
                    $installed_dir =~ s|\.$||g;
                    print_debug("installed_dir=$installed_dir\n");
                    next;
                }
                next if (!defined($_) || ($_ eq '') || ($_ eq '.') || ($_ eq '..'));
                my $installed_file = "/$_";
                $installed_file =~ s|^$installed_dir||g;
                hash_devtools_file($action, $package_id, "$developer_dir/$installed_file");
            }
            close(PKGFILES);

            if ((-d $installed_dir) && ($installed_dir ne '/')) {
                hash_devtools_file($action, $package_id, $installed_dir);
            }
        }
    } elsif ($package_id =~ m|^(.*)\.pkg$|) {
        my $package_name = $1;
        my $package_path = "$receipts_dir/$package_id";
        my $package_bom_path = undef;
        foreach my $package_bom_candidate (
            "$package_path/Contents/Archive.bom",
            "$package_path/Contents/Resources/Archive.bom",
            "$package_path/Contents/Resources/$package_name.bom",
        ) {
            if ((-f $package_bom_candidate) && (-r $package_bom_candidate)) {
                $package_bom_path = $package_bom_candidate;
                last;
            }
        }

        if (defined($package_bom_path)) {
            my $installed_dir = undef;
            my $count = 0;
            foreach (execute_command(['/usr/bin/lsbom'],['-f','-l','-d','-p','f',$package_bom_path])) {
                s|\s*$||g;
                if (++$count == 1) {
                    $installed_dir = "/$_";
                    $installed_dir =~ s|\.$||g;
                    print_debug("installed_dir=$installed_dir\n");
                    next;
                }
                next if (!defined($_) || ($_ eq '') || ($_ eq '.') || ($_ eq '..'));
                s|^\./||g;
                my $installed_file = "/$_";
                $installed_file =~ s|^$installed_dir||g;
                hash_devtools_file($action, $package_id, "$developer_dir/$installed_file");
            }

            if ($installed_dir && (-d $installed_dir) && ($installed_dir ne '/')) {
                hash_devtools_file($action, $package_id, $installed_dir);
            }
        }
    }
}

####################################################################################################

sub hash_devtools_file {
    my $action = shift;
    my $package_id = shift;
    my $file_name = shift;

    return if (!$file_name);

    my $file_type;

    if ($action eq 'add') {
        if (exists($devtools_file_hash{$file_name})) {
            $file_type = $devtools_file_hash{$file_name};
        } else {
            $file_type = file_type($file_name);
            $devtools_file_hash{$file_name} = $file_type;
        }
        print_debug("hash_devtools_file $package_id add $file_type $file_name\n");
    } elsif ($action eq 'sub') {
        if (exists($devtools_file_hash{$file_name}))  {
            $file_type = $devtools_file_hash{$file_name};
            delete($devtools_file_hash{$file_name});
        } else {
            $file_type = 'x';
        }
        print_debug("hash_devtools_file $package_id sub $file_type $file_name\n");
    }
}

####################################################################################################

sub remove_devtools_files {
    print("Removing devtools files...\n");

    foreach my $file_name (sort keys %devtools_file_hash) {
        my $file_type = $devtools_file_hash{$file_name};
        if (($file_type eq 'f') || ($file_type eq 'l')) {
            remove_devtools_file($file_name);
            delete($devtools_file_hash{$file_name});
        }
    }
}

####################################################################################################

sub remove_devtools_file {
    my $file_name = shift;
    return if (!$file_name);

    my $dirname = dirname($file_name);
    my $basename = basename($file_name);
    my $ufs_rsrc_file_name = "$dirname/._$basename";

    if ((!-f $file_name) && (!-l $file_name)) {
        print_warning("File '$file_name' not found on disk.\n");
        return;
    }

    print_verbose("Removing file: '$file_name'\n");
    if (-f $ufs_rsrc_file_name) {
        print_verbose("Removing file: '$ufs_rsrc_file_name'\n");
    }

    if ($do_nothing != 1) {
        if ((unlink($file_name)) != 1) {
            print_error("Unable to remove '$file_name': $!\n");
        }

        # If there's an associated resource file, delete it too
        if ((-f $ufs_rsrc_file_name) && ((unlink($ufs_rsrc_file_name)) != 1)) {
            print_error("Unable to remove '$ufs_rsrc_file_name': $!\n");
        }
    }
}

####################################################################################################

sub remove_devtools_package_receipts {
    foreach my $package_id (sort keys %devtools_package_hash) {
        remove_package_receipt($package_id, 1);
    }
}

####################################################################################################

sub remove_empty_devtools_directories {
    print("Removing empty devtools directories...\n");

    foreach my $file_name (reverse (sort keys %devtools_file_hash)) {
        my $file_type = $devtools_file_hash{$file_name};
        if ($file_type eq 'd') {
            remove_directory_if_empty($file_name);
            delete($devtools_file_hash{$file_name});
        }
    }
}

####################################################################################################
# Recursively scan a directory and delete it only if all the directories below it are empty.
####################################################################################################

sub remove_directory_if_empty {
    my $directory_name = shift;
    return if (!$directory_name);

    print_debug("remove_directory_if_empty '$directory_name'\n");

    if ($directory_name && opendir(DIR,$directory_name)) {
        while (my $file_name = readdir(DIR)) {
            if (($file_name ne '.') && ($file_name ne '..')) {
                my $path = "$directory_name/$file_name";
                if (-d $path) {
                    remove_directory_if_empty($path);
                }
            }
        }
        closedir(DIR);
    }

    if (is_empty_directory($directory_name) == 1) {
        print_verbose("Removing empty directory: '$directory_name'\n");
        if ($do_nothing != 1) {
            unlink("$directory_name/.DS_Store");
            if (!rmdir($directory_name)) {
                print_warning("Unable to remove empty directory '$directory_name': $!\n");
            }
        }
    }
}

####################################################################################################
# Determine whether a directory is empty for the purposes of deletion.  If a directory contains
# nothing but .DS_Store then we consider it to be empty.
####################################################################################################

sub is_empty_directory {
    my $directory_name = shift;
    my $result = 0;

    if ($directory_name && opendir(DIR,$directory_name)) {
        $result = 1;
        while (($result == 1) && (my $file_name = readdir(DIR))) {
            if (($file_name ne '.') && ($file_name ne '..') && ($file_name ne '.DS_Store')) {
                $result = 0;
            }
        }
        closedir(DIR);
    }

    return $result;
}

####################################################################################################
# Remove the receipt for the specified package id.
# Don't delete anything unless the caller really means it.
####################################################################################################

sub remove_package_receipt {
    my $package_id = shift;
    return if (!$package_id);

    my $really = shift;
    if (!$really) {
        print_warning("You didn't really mean to remove the receipt for package $package_id\n");
        return;
    }

    print_verbose("Removing receipt for package: '$package_id'\n");

    $package_id =~ s|/||g;    # There shouldn't be any path seps in the pkg name...
    return if (($package_id eq '') || ($package_id eq '.') || ($package_id eq '..'));

    my @command_args;
    my $return_code;

    if ($package_id =~ m|^com\.apple\.pkg\..*$|) {
        my $package_db_file = "$installedfiles_dir/$package_id";
        return if (!-f $package_db_file);

        @command_args = (
            'rm',
            '-rf',
            $package_db_file,
        );
        print_verbose("@command_args\n");
        if ($do_nothing != 1) {
            system(@command_args);
            $return_code = $? >> 8;
            if ($return_code != 0) {
                print_warning("There may have been a problem removing the package db file '$package_id'\n");
            }
        }
    } elsif ($package_id =~ m|^.*\.pkg$|) {
        my $package_directory = "$receipts_dir/$package_id";
        return if (!-d $package_directory);

        @command_args = (
            '/bin/rm',
            '-rf',
            $package_directory,
        );
        print_verbose("@command_args\n");
        if ($do_nothing != 1) {
            system(@command_args);
            $return_code = $? >> 8;
            if ($return_code != 0) {
                print_warning("There may have been a problem removing the package directory '$package_directory'\n");
            }
        }
    }
}

####################################################################################################

sub remove_file_list {
    my $file_list_ref = shift;

    foreach my $file_name (@$file_list_ref) {
        if (($file_name) && (-e $file_name)) {
            my @command_args = (
                '/bin/rm',
                '-rf',
                $file_name,
            );
            print_verbose("@command_args\n");
            if ($do_nothing != 1) {
                system(@command_args);
                my $return_code = $? >> 8;
                if ($return_code != 0) {
                    print_warning("There may have been a problem removing the file '$file_name': $!\n");
                }
            }
        }
    }
}

####################################################################################################

sub remove_generated_files {
    print("Removing generated files...\n");

    my @file_list = (
        "$developer_dir/usr/share/man/whatis",
    );

    if ($developer_dir && opendir(DIR,$developer_dir)) {
        while (my $file_name = readdir(DIR)) {
            if (($file_name ne '.') && ($file_name ne '..')) {
                if ($file_name =~ /^Icon/) {
                    push(@file_list, "$developer_dir/$file_name");
                }
            }
        }
        closedir(DIR);
    }

    remove_file_list(\@file_list);
}

####################################################################################################

sub remove_library_cache_xcode_files {
    print("Removing Xcode Caches...\n");

    system("find /var/folders -name 'com.apple.Xcode*' -exec rm -rf {} \\;");

    system("rm -rf /Library/Caches/com.apple.DeveloperTools.*");
}

####################################################################################################

sub file_type {
    my $file_name = shift;
    my $file_type = (!$file_name) ? 'x' : (-f $file_name) ? 'f' : (-l $file_name) ? 'l' : (-d $file_name) ? 'd' : (-e $file_name) ? 'e' : 'x';
    return $file_type;
}

####################################################################################################

sub execute_command {
    my $command_prog_ref = shift;
    my $command_list_ref = shift;

    my @result_list = ();

    if (open(CHILD, "-|")) {
        while (<CHILD>) {
            chomp;
            push(@result_list,$_);
        }
        close(CHILD);
    } else {
        exec(@$command_prog_ref, @$command_list_ref);
    }

    return @result_list;
}

####################################################################################################

sub print_verbose {
    return if ($verbose != 1);
    my $msg = shift;
    print $msg;
}

####################################################################################################

sub print_warning {
    return if ($warning != 1);
    my $msg = shift;
    print STDERR "WARNING: $msg";
}

####################################################################################################

sub print_error {
    my $msg = shift;
    print STDERR "ERROR: $msg";
}

####################################################################################################

sub print_debug {
    return if ($debug != 1);
    my $msg = shift;
    print STDERR "DEBUG: $msg";
}

####################################################################################################

main();

####################################################################################################
